﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Luna;
using Luna.Framework;
using Luna.Framework.Manifest;
using VulnDBExplorer.Services;
using VulnDB.Action.DataCollectors.Services.CodeAge;
using VulnDBExplorer.Services.Impl;
using System.IO;
using System.Text.RegularExpressions;
using VulnDBExplorer.Framework.Impl;

namespace VulnDB.Action.DataCollectors.Actions {
	class ProcessCVSAnnotationFileAction: MultiThreadDataCollector<AnnotationJob> {

		public ProcessCVSAnnotationFileAction() {
			NumberOfWorker = 1;
			MaxQueueCount = 20;
			this.ExecutionMode = ActionExecutionMode.SeparatedThread;
		}

		protected ISourceProfileStorage Storage { get; set; }

		protected StreamReader Input { get; set; }

		protected ICodebase Codebase { get; set; }

		protected override object onJobExecute(AnnotationJob job) {
			if (Codebase is CVSCodebase)
				processCVSAnnotationJob(job);
			else
				processHgAnnotationJob(job as HgAnnotationJob);
			return null;
		}

		private void processCVSAnnotationJob(AnnotationJob job) {
			SourceFileProfile profile = new SourceFileProfile(job.Filename);
			Match m = CodeProfile.CVSLinePattern.Match(job.Text);
			int lineidx = 1;
			while (m.Success) {
				profile.addLine(m.Groups[1].Value, m.Groups[3].Value,
							DateTime.Parse(m.Groups[4].Value), lineidx);
				lineidx++;
				m = m.NextMatch();
			}
			lock (Storage)
				Storage.writeProfile(profile);
		}

		private void processHgAnnotationJob(HgAnnotationJob job) {
			// determine active file
			List<SourceFileProfile> files = new List<SourceFileProfile>(job.Files.Values);
			files.Sort((f1, f2) => f1.Filename.CompareTo(f2.Filename));
			int newestFileIdx = 0;
			for (int i = 1; i < files.Count; i++)
				if (getMaxRevisionOfFile(files[i]) > getMaxRevisionOfFile(files[newestFileIdx]))
					newestFileIdx = i;

			// consolidate each file, and save it to server
			foreach (var item in files) {
				if (!Storage.hasProfile(item.Filename, files[newestFileIdx].Filename)) {
					item.ActiveFilename = files[newestFileIdx].Filename;
					foreach (var rev in item)
						rev.consolidateLines();
					try {
						lock (Storage)
							Storage.writeProfile(item);
					}
					catch (Exception ex) {
						Console.writeLine("Error saving file {0}, job {1}", item.Filename, job.JobIndex);
						throw ex;
					}
				}
				else
					Console.writeLine("[Job: {0}] Skipped file: {1}", job.JobIndex, item.Filename);
			}
		}

		private int getMaxRevisionOfFile(SourceFileProfile f) {
			int max = 0;
			foreach (var item in f)
				if (item.Revision.toInt() > max) max = item.Revision.toInt();
			return max;
		}

		protected override void onJobCompleted(AnnotationJob job, object result) {
			
		}

		protected override void setupExecution(ActionExecutionArgs arg) {
			if (arg.Data is IConfigurationManifest) {
				IConfigurationManifest m = (arg.Data as IConfigurationManifest);
				Codebase = (ICodebase)Framework.resolveObject(arg.Context,
						new Uri(m.getProperty("codebase", "")), arg.Data);
				string annotationFile = m.getProperty("annotatedFile", "");

				Storage = new SqlSourceProfileStorage(Codebase, "ranarog", "CodeProfile");

				if (annotationFile.fileExists() && Codebase != null) {
					Input = new StreamReader(annotationFile, UTF8Encoding.UTF8, false, 10 * 1024 * 1024);
					ProgressMonitor = Application.createProgressMonitor();
					ProgressMonitor.start("Processing annotation file...", (int)Input.BaseStream.Length);
					Console.writeLine("Codebase: {0}", Codebase.Name);
					Console.writeLine("Annotation file: {0}:", annotationFile);
				}
			}
		}

		protected override void onUpdateProgress(object status) {
			TimeSpan elapsed = Watch.Elapsed;
			if (Input.BaseStream.Position < Input.BaseStream.Length) {
				long curPos = Input.BaseStream.Position > 0 ? Input.BaseStream.Position : 1;

				long remaintTick = elapsed.Ticks * (Input.BaseStream.Length - curPos) / curPos;

				ProgressMonitor.update((int)Input.BaseStream.Position, (int)Input.BaseStream.Length);

				ProgressMonitor.Description = string.Format("Ellapsed: {0}. Remain estimated: {1}", elapsed.ToString().Substring(0, 8), new TimeSpan(remaintTick).ToString().Substring(0, 8));
			}
			else {
				ProgressMonitor.update(1, JobPool.Count);
				ProgressMonitor.Description = string.Format("Ellapsed: {0}. Saving pending processed jobs ({1})...", elapsed.ToString().Substring(0, 8), JobPool.Count);
			}
		}

		protected override void finalizeExecution() {
			stopProgressUpdateTimer();
			if (ProgressMonitor != null)
				ProgressMonitor.stop();
			Console.writeLine("Ellapsed time: {0}", Watch.Elapsed.ToString());
		}

		protected override IEnumerable<AnnotationJob> enumJobs() {
			
			startProgressUpdateTimer(1000);
			if (Codebase is CVSCodebase) {
				bool jobDetected = false;
				StringBuilder b = new StringBuilder();
				string filename = null;
				while (!Input.EndOfStream) {
					string line = Input.ReadLine();
					if (line.StartsWith("Annotations for")) {
						if (jobDetected)
							yield return new AnnotationJob() { Text = b.ToString(), Filename = filename };
						int idx = line.LastIndexOf("mozilla/");
						filename = line.Substring(idx + 8).TrimEnd();
						if (filename.EndsWith(".h") || filename.EndsWith(".c") || filename.EndsWith(".cpp")) {
							if (!(jobDetected = !Storage.hasProfile(filename)))
								Console.writeLine("File skipped: " + filename);
						}
						else
							jobDetected = false;
						if (jobDetected) b.Clear();
					}
					else if (jobDetected)
						b.AppendLine(line);
				}
			}
			else {
				SourceFileProfile profile = null;
				HgAnnotationJob job = null;
				int lastLineNo = -1;
				while (!Input.EndOfStream) {
					string line = Input.ReadLine();
					Match m = CodeProfile.HgLinePattern.Match(line);
					if (m.Success) {
						string author = m.Groups[1].Value;
						string revision = m.Groups[2].Value;
						DateTime age = DateTime.Parse(m.Groups[3].Value + " " + m.Groups[4].Value);
						string filename = m.Groups[5].Value.Trim();
						int lineNo = m.Groups[6].Value.toInt();
						if (lineNo == 1 && lastLineNo != 1) { // new file detected
							/* TODO: when two consecutive lines with line index 1 detected, we considered they are
							 * actually in a same file. 
							 * This workaround applies to: build/wince/tools/arm-wince-gcc.c
							 */
							if (job != null)
								yield return job;
							job = new HgAnnotationJob() { JobIndex = job != null ? job.JobIndex + 1 : 0 };
						}
						profile = job.Files.defaultGet(filename);
						if (profile == null)
							job.Files.Add(filename, profile = new SourceFileProfile(filename));
						profile.addLine(revision, author, age, lineNo);
						lastLineNo = lineNo;
					}
					else
						Console.writeLine("Unmatched lines: {0}", line);
				}
			}
		}
	}

	class AnnotationJob {
		public string Text { get; set; }
		public int JobIndex { get; set; }
		public string Filename { get; set; }
	}

	class HgAnnotationJob: AnnotationJob {
		
		public HgAnnotationJob() {
			Files = new Dictionary<string, SourceFileProfile>();
		}

		public Dictionary<string, SourceFileProfile> Files { get; private set; }
	}
}
